Clean Architecture
第1部 Introduction
何かを「一度だけ」動かすのは、それほど難しいことではない。しかし、ソフトウェアを正しくするのは難しい。
ソフトウェアアーキテクチャの目的は、求められるシステムを構築・保守するために必要な人材を最小限に抑えることである。
ケーススタディ
ある会社は、8回のリリースを経て、生産性の97%を失った。
生産性の変化の原因はなんだろうか?誰もが一生懸命に働いていて、誰かが手を抜いているわけではない。
開発者は、優れた、クリーンな、上手く設計されたコードが重要であることがわかっていなかった。
「あとでクリーンにすればいい。先に市場に出さなければ」
後でコードがクリーンになることはない。なぜなら、business pressure が止むことはないからだ。そして崩壊が始まる。生産性がゼロに近づいていく。
ソフトウェアの価値は大別して「振る舞い」と「アーキテクチャ」の2つがある。
「重要」と「緊急」
「振る舞い」は緊急だが、常に重要とは限らない。
「アーキテクチャ」は重要だが、常に緊急とは限らない。
よくある間違いは、「緊急だが重要でない」と「緊急かつ重要」を区別できていないこと
ビジネスマネージャはアーキテクチャの重要性を評価できない。そのためにソフトウェア開発者が雇われている。
ソフトウェア開発者は、アーキテクチャに対して責任がある。struggle すること。ソフトウェアを保護すること。それがあなたの役割であり、義務であり、あなたが雇われている大きな理由だ。
第3部 Design Principle
Single Responsibility Principle
"A module should have one, and only one, actor"
actor は、そのソフトウェアが満たすべきビジネス要件を持っているユーザーやステークホルダーを指す
例: Employee クラス
経理チームは Employee に対して賃金を計算したい
労務チームは Employee に対して労働時間を計算したい
技術チームは Employee に対してデータベースへの永続化を行いたい
このとき、Employee は3つの actor を持つので、3つのクラスに分解すべき
Open Closed Principle
"A software artifact should be open for extension but closed for modification"
ソフトウェアアーティファクトの振る舞いは、そのアーティファクトを変更することなしに拡張できるべき
拡張 = 新機能の追加、変更 = 既存のコードの変更
例: financial summary を表示するウェブサービス
現在の仕様では、ページはスクリーン上に表示され、負の数値は赤く表示されることになっている
ステークホルダーが同じ情報をモノクロプリンタで出力するように要求してきたとする
ページは適切に改ページされ、ページヘッダ、ページフッタ、カラムラベルを付与する。
負の数値は括弧で囲む
このとき、既存の コードをどれだけ変更する必要があるか?
UI をビジネスロジックから分離していれば、既存のコードをあまり変更しなくてもこの機能を実装できる
Liskov Substitution Principle
"型 S の各オブジェクト o1 に対して、型 T のオブジェクト o2 が存在し、T を使って定義された全てのプログラム P に対して、o2 を o1 に置換しても P の振る舞いが変化しないならば、S は T のサブタイプである"
LSP が破られている例:
タクシー派遣サービスの集約サービスを構築しているとする
各タクシー派遣サービスは共通の API を提供しており、集約サービスはそれを利用する
しかし、Acme Taxi は API を間違って実装しており、しかも修正できないため、集約サービスに if (driver.getDispatchUri().startsWith("acme.com")) みたいなコードを書いた
LSP 違反 (呼び出し元の振る舞いが変わっている)
Acme Taxi が Purple Taxi を買収し、システム統合を行ったら?
Interface Segregation Principle
以下の状況を考える:
OPS クラスと、User1, User2 クラスがある
OPS クラスは op1, op2 メソッドを提供する
User1 は op1 のみを利用し、User2 は op2 のみを利用する
User2 は op1 を利用しないが、op1 に依存してしまっている。
op1 が変更されたら、User2 の再コンパイルして再デプロイする必要がある
OPS を直接使うのではなく、U1Ops, U2Ops というインターフェイスを定義し、それを使う
"利用しないものに依存してはいけない"
Dependency Inversion Principle
"source code dependencies refer only to abstractions, not to concretions"
volatile concretions に依存するのではなく、stable abstract interface を利用すること
コーディングプラクティス
Don't refer to volatile concrete classes
Don't derive from volatile concrete classes
Don't override concrete functions
Never mention the name of anything concrete and volatile
第4部 Component Principle
コンポーネント = デプロイメントの単位
Component Cohesion
Reuse/Release Equivalence Principle
"The granule of reuse is the granule of release"
ひとつのコンポーネントに配置されたクラスやモジュールは、一緒にリリース可能でなければならない
同じバージョン番号を持つ
同じリリースドキュメントに含められる
They _make sense_ both to the author and to the users
Common Closure Principle
"Gather into components those classes that change for the same reasons and the same times"
アプリケーションに変更が必要な場合、変更が複数のコンポーネントにまたがって発生するのではなく、ひとつのコンポーネントに閉じているのが望ましい
Common Reuse Principle
"Don't force users of a component to depend on things they don't need"
緊密に関連していないクラスを同じコンポーネントに配置してはならない。
利用者があるコンポーネントを利用するときは、それに含まれる全てのクラスが使われるのが理想
REP, CCP, CRP の関係
REP と CCP は inclusive な原理: コンポーネントを大きくしようとする
CRP は exclusive な原理: コンポーネントを小さくしようとする
現在の 関心に合うようなポジションを見つけるべき
例えば、初期の開発においては CRP よりも CCP が重要
関心は移り変わっていくことを認識しておくこと
Component Coupling
Acyclic Dependencies Principle
"Allow no cycles in the component dependency graph"
依存グラフに閉路があるようなら、DIP を適用して閉路を取り除くこと
Stable Dependencies Principle
"Depend in the direction of stability"
自分よりも stable なコンポーネントにのみ依存してよい
stable とは?
変化しにくいこと
コンポーネントX の stability は、X に依存しているコンポーネントの数でモデル化できる
stability metrics
I: 不安定度
I = 依存先の数 / (依存先の数 + 依存元の数)
全てのコンポーネントが stable である必要はない
unstable なコンポーネントは簡単に変更できる
Stable Abstractions Principle
"A component should be as abstract as it is stable"
コンポーネントが stable であるなら、インターフェイスや抽象クラスで構成されていないといけない
abstraction metrics
A: 抽象度
A = 抽象クラスやインターフェイスの数 / クラス数
I と A の関係
Zone of Pain: (I, A) = (0, 0)
つまり highly stable かつ concrete なものは辛い
Zone of Uselessness: (I, A) = (1, 1)
つまり highly unstable かつ abstract なものは存在意義がない
バランスがいいのは (1, 0) と (0, 1) を結ぶ斜めの直線の近く
この直線を Main Sequence と呼ぶ
第5部 Architecture (書きかけ)
アーキテクチャとは何か?アーキテクトがすべきなのは何か?
ソフトウェアアーキテクトは、プログラマーである
"アーキテクトはハイレベルな問題に集中するためにコードから手を引くべき" というような嘘に騙されないこと!
アーキテクチャにおいて重要なことは、できるだけ多くの選択肢をできるだけ長い期間に渡って保持すること。
システムを正しく動かすことではない。(正しくは動くけど酷いアーキテクチャなシステムはいっぱいある)
全てのソフトウェアは2つの要素に分解できる: policy and details
policy はビジネスルールや手続きを具象化したもの
システムの真の価値は policy にある
details はヒトや他のシステムが policy とコミュニケートできるようにするもの
IO デバイス、データベース、web システム、サーバー、フレームワーク、コミュニケーションプロトコルなどは全て details である
アーキテクチャのゴールは、policy をシステムの最も根本的な要素として認識し、details を policy と切り離すこと。これにより、details に関する決定を遅らせることができる。
開発の初期段階において、データベースを選ぶ必要はない。policy はデータベースが relational、distributed、hierarchical、あるいは単なるプレインテキストであることに依存しない。
開発の初期段階において、ウェブサーバーを選ぶ必要はない。policy は HTML、AJAX、JSP、JSF などに依存しない。
開発の初期段階において、REST を採用する必要はない。policy は外部世界のインターフェイスとは無関係。
開発の初期段階において、dependency injection framework を採用する必要はない。policy は依存関係の解決手段に依存すべきでない。
例: device independence
1960年代、我々はIOデバイスを直接使うコードを書いていた。
筆者が書いた PDP-8 プログラムは、パンチカードのリーダーを直接操作してデータを読んでいた。
しかし、パンチカードは落として順番がぐちゃぐちゃになったりして不便だったので、磁気テープに移行しようとした。
しかし、全てのソフトウェアはカードリーダーとカードパンチャーを直接操作するように書かれていたため、全てのプログラムを磁気テープを使うように書き直さなければならず、とても大きなコストがかかった。
その後、device independence の概念が発見された。オペレーティングシステムがIOデバイスを抽象化し、同じプログラムが全く変更なしにカードでもテープでも使えるようになった。
境界
境界とは、ソフトウェアの要素を切り分けるもの
境界は、境界の片側からもう片側を知ることを制限する
システムを構築・メンテするためのヒューマンリソースを浪費するものはなにか?――それは、密結合である
特に、時期尚早な決断による密結合である
例:
P社は1990年代にデスクトップGUIアプリケーションを売って成功していた。
1990年代後半になってwebが台頭してきた。P社はこのアプリケーションをwebに載せることにした。
P社のプログラマは、サーバーファームの夢を見て、three-tiered "architecture" を考えた。
P社のプログラマは、とても早い段階において、全てのドメインオブジェクトは GUI tier、middleware tier、database tier に instantiations を持つという決定をした。これらの instantiation は異なるマシンに配置され、プロセッサ間、tier間のコミュニケーションがセットアップされた。tier間のメソッド呼び出しは、オブジェクトに変換され、シリアライズされ、マーシャルされた。
「既存のレコードにフィールドを追加する」といった単純な機能でも、これら全ての tier のクラスとtier間メッセージにフィールドを追加する必要があった。データは双方向に移動するため、4つのメッセージプロトコルを修正する必要があった。それぞれのプロトコルは送信側と受信側があるため、8つのプロトコルハンドラが必要だった。
しかし、P社はサーバーファームが必要なシステムを売ることはなかった。全てのシステムは単一のサーバーにデプロイされた。ひとつのサーバーの上で object instantiations が行われ、serialization が行われ、marshaling が、de-marshaling が、message の構築とパースが、ソケット通信が行われた。
Use case
システムの意図がアーキテクチャのレベルで読み解けるようにすること
ショッピングカートアプリケーションの場合、そのアーキテクチャからショッピングカートであることがわかるようにすること
TODO